3 UI 事件
3.1 鼠标事件
该事件不仅来自鼠标,也可能是其他兼容性设备模拟鼠标操作(平板、手机)。
3.1.1 常见鼠标事件
鼠标:
-
mousedown/mouseup: 在元素上点击 / 释放。 -
mouseover/mouseout: 从一个元素上移入 / 移出。 -
mousemove: 在元素上的移动就会触发。 -
click: 鼠标左键触发。在发生mousedown及mouseup这两个事件后,会触发该事件。 -
dblclick: 在短时间内双击同一元素后触发,很少使用。 -
contextmenu: 鼠标右键按下时触发。- 还有其他打开菜单的方式。比如特定的键盘按键也会触发,因此它不完全是鼠标事件。
3.1.2 事件顺序
鼠标事件的触发之间是有先后顺序的,比如:
- 一次左键单击事件:
mousedown-->mouseup-->click - 一次左键双击事件:
mousedown-->mouseup-->click-->mousedown-->mouseup-->click-->dblclick
3.1.3 事件属性 - 鼠标按钮
点击事件(mousedown, mouseup, click, dblclick, contextmenu)都会拥有一个 event.button 属性,用来保存触发事件的鼠标按键状态:
| 鼠标按键状态 | event.button |
|---|---|
| 左键 (主要按键) | 0 (常见) |
| 中键 (辅助按键) | 1 |
| 右键 (次要按键) | 2 (常见) |
| X1 键 (后退按键) | 3 |
| X2 键 (前进按键) | 4 |
3.1.4 事件属性 - 组合键
鼠标事件包含了组合键信息,以下是事件属性。如果在事件时,按下了相应的按键,则对应会置为 true。
event.shiftKey:Shift;event.altKey:Alt(或对于 Mac 是 Opt);event.ctrlKey:Ctrl;event.metaKey:对于 Mac 是 Cmd。
注意:在 Mac 上,通常使用 cmd 代替 ctrl。所以,在判断用户是否按下 ctrl 组合键时,要这样检查:
if (event.ctrlkey || event.metakey)
3.1.5 事件属性 - 坐标
所有的鼠标事件都提供了两种形式的坐标:
- 相对于视口的坐标:
clientX和clientY。 - 相对于文档的坐标:
pageX和pageY。
3.1.6 干扰
鼠标事件有事会有副作用,在某些界面中可能会出现干扰:
- 双击事件:比如双击一个文本,除了会触发我们设定的
dblclick事件外,还会选择文本。 - 按下鼠标:在按下鼠标左键,不松开的情况下拖动鼠标,也会触发选中文本。
解决方案,阻止 mousedown 事件中,浏览器的默认行为:
- 使用
return false:<b ondblclick="alert('Click!')" onmousedown="return false">XXXX</b>
3.1.6.1 防止复制
额外的tips,如何防止浏览器中,用户的复制行为,保护文本不被复制:
<div oncopy="alert('不允许复制!'); return false">
这里是不允许复制的文本内容。
</div>
使用 oncopy 特性,返回 false,在用户尝试右键点击复制的时候,就会触发 oncopy 中的代码,弹出提示框,最终会失败。
3.2 移动鼠标
-
mousedown/mouseup: 在元素上点击 / 释放。 -
mouseover/mouseout: 从一个元素上移入 / 移出。
3.2.1 事件属性 - event.relatedTarget
relatedTarget属性是对target的补充。relatedTarget的值可以为null,表明可能是鼠标从另一个窗口过来(over)、或移动到了另一个窗口上(out)。
当鼠标从 A 元素离开,已经移动到了 B 元素时:
-
对于
mouseover: -
event.target:鼠标移到的当前元素 —— B 元素。 -
event.relatedTarget:鼠标之前所处的元素 —— A 元素。 -
对于
mouseout,与 over 相反,记住 over 就行:event.target:鼠标之前所处的元素 —— A元素。event.relatedTarget:鼠标移到的当前元素 —— B 元素。
记: target 属性是我们的主要目的,relatedTarget 属性是我们为了方便而增添的附加信息。
- 所以,对于
mouseover我们主要关注的是也就是当前鼠标所处的位置(over),这个值自然是保存到target中。
3.2.3 元素的跳过
mousemove事件,是随着鼠标的移动而触发。浏览器会间隔很小的周期,不断的重复检查鼠标的坐标位置,用以确定是否触发 mousemove 事件。
- 通过
mousemove事件 ,浏览器就可以计算出mouseover事件;通过mouseover事件,浏览器就可以监听到mouseout事件。
这意味着,当鼠标移动的速度非常快,可能在这个“小的周期”中,鼠标一下划过过了多个元素,这就会导致浏览器没有及时检测到鼠标具体划过了哪几个元素,造成了元素的跳过。
-
如果鼠标从上图所示的
#FROM快速移动到#TO元素,则中间的<div>元素可能会被跳过。mouseout事件可能会在#FROM上被触发,然后立即在#TO上触发mouseover。 -
如果
mouseover被触发了,则mouseout也一定会触发,这两者是一一对应的。- 如果鼠标指针“正式地”进入了一个元素(生成了
mouseover事件),那么一旦它离开,我们就会得到mouseout。
- 如果鼠标指针“正式地”进入了一个元素(生成了
3.2.4 mouseover 的细节
先说原则:
- 鼠标指针移动到嵌套最多的那个元素上,也就是视觉上最突出的那个元素上(z-index最大的那个),就会触发
mouseover事件。 - 可以非常笼统的说,在视觉上分割出的区域(子元素和父元素在视觉上是两个区域),鼠标在这两个区域移动,就会触发 over,out
以下分两种情况讨论:
3.2.4.1 父元素 ==> 子元素
当鼠标从父元素移动到子元素时,在父元素上就会触发 mouseout 事件,在子元素上就会触发 mouseover 事件。
- 如果设置了事件会发生捕获,则子元素上如果设置了
mouseover事件,也会被触发。
3.2.4.2 子元素 ==> 父元素
当鼠标从子元素移动到父元素是,在子元素上就会触发 mouseout 事件,在父元素上就会触发 mouseover 事件。
- 与此同时,由于默认情况下事件会冒泡。因此,如果父元素上设置了
mouseout的事件处理程序,也会触发mouseout的回调。- 注意:此时触发的
mouseout是归属于子元素的,所以虽然因冒泡在父元素上也触发了该事件,但其属性event.target的值,依然同子元素上完全相同。
- 注意:此时触发的
因此,如果要判断鼠标是否离开了父元素和其嵌套的子元素,不能单单判断父元素上是否触发了 mouseout,而是要具体判断:
-
event.target的值是不是父元素。如果是,才能证明触发事件的元素,就是父元素本身。 -
或,
event.relatedTarget的值是不是子元素。如果是,证明鼠标是从子元素移动到父元素上,而不是从外部移动到父元素。 -
或,
mouseenter和mouseleave事件。
3.2.5 mouseenter 和 mouseleave
事件 mouseenter/mouseleave 类似于 mouseover/mouseout。它们在鼠标指针进入/离开元素时触发。
但是有两个重要的区别:
- enter 和 leave 事件,元素内部与后代之间的转换不会产生影响。
- 同时,事件
mouseenter/mouseleave不会冒泡。
当鼠标指针进入一个元素时,会触发 mouseenter,当鼠标指针离开该元素时,事件 mouseleave 才会触发。
- 与 over/out 的显著区别,就是没有了子元素嵌套的概念。只要还处在父元素中,即便是进入了更深的子元素,也依然不会触发
mouseleave直到完全离开的父元素,才会触发。
3.2.6 事件委托
利用 mouseover 和 mouseout 可以建立事件委托,简单的例子如下:
在列表的 <ul> 上设置 mouseover 监听,利用对 event.target 属性值,可以判断出当前鼠标在其子元素中的哪一个位置。
<ul id="test">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#test');
ul.onmouseover = function(event) {
let text = event.target.firstChild; // 获取li标签中包含的文本值
console.log(text); // 当鼠标移动到某个li中,就会监听到,然后在控制台输出文本值:"1", "2"或"3"。
}
</script>
相反,mouseenter 和 mouseleave 由于忽略了父子元素的关系,不可以使用事件委托来监听。
3.3 拖放鼠标
3.3.1 算法
鼠标的拖放,简单来说就是三个步骤:鼠标按下、鼠标拖动、鼠标释放,对应了三个事件监听:mousedown, mousemove, mouseup。
基础的拖放算法,在触发相关事件时,通常要做出如下行为:
mousedown: 设置好准备移动的元素,可能是创建一个副本,也可能是设置他的position: absolute。mousemove:通过更改position:absolute情况下的left/top来移动它。mouseup:执行与完成的拖放相关的所有行为。
有好几个应用,值得[反复记忆](鼠标拖放事件 (javascript.info))。
3.4 指针事件
3.4.1 历史
-
很早以前,只有鼠标事件。
-
引入了触摸事件。有了手机和平板电脑,触摸设备比鼠标具有更多的功能。例如,多点触控。鼠标事件并没有相关属性来处理这种多点触控。
例如
touchstart、touchend和touchmove,它们具有特定于触摸的属性(这里不再赘述这些特性,因为指针事件更加完善)。不过这还是不够完美。很多输入设备(如触控笔)都有自己的特性。而且同时维护鼠标事件和触摸事件的代码,非常笨重。
-
引入了全新的规范「指针事件」。为各种指针输入设备提供了一套统一的事件。
注: IE 10 或 Safari 12 或更低的版本不兼容指针事件。
3.4.2 指针事件类型
指针事件的命名方式和鼠标事件类似:
| 指针事件 | 类似的鼠标事件 |
|---|---|
pointerdown | mousedown |
pointerup | mouseup |
pointermove | mousemove |
pointerover | mouseover |
pointerout | mouseout |
pointerenter | mouseenter |
pointerleave | mouseleave |
pointercancel | - |
gotpointercapture | - |
lostpointercapture | - |
3.4.3 指针事件属性
指针事件具备和鼠标事件完全相同的属性,包括 clientX/Y 和 target 等。
以及一些其他属性:
pointerId:触发当前事件的指针唯一标识符。浏览器生成的,解决多指针同时触发的问题。pointerType:指针的设备类型,必须为字符串。可以是:“mouse”、“pen” 或 “touch”。- 我们可以针对不同类型的指针输入做出不同响应。
isPrimary:当指针为首要指针 (多点触控时按下的第一根手指)时为true。
有些指针设备会测量接触面积和点按压力(指压在触屏上),有很少使用的属性配合:
width:指针(例如手指)接触设备的区域的宽度。对于不支持的设备(如鼠标),这个值总是1。height:指针(例如手指)接触设备的区域的长度。对于不支持的设备,这个值总是1。pressure:触摸压力,一个介于 0 到 1 之间的浮点数。对于不支持的设备,这个值总是0.5(按下时)或0(未按下时)。tangentialPressure:归一化后的切向压力(tangential pressure)。tiltX,tiltY,twist:针对触摸笔的几个属性,用于描述笔和屏幕表面的相对位置。
3.4.4 多点触控
我们可以通过 pointerId 和 isPrimary 属性的帮助,处理多点触控。
当用户用一根手指触摸在触摸屏的某个位置,然后将另一根手指放在该触摸屏的其他位置时,会发生以下情况:
- 第一个手指触摸:
pointerdown事件触发,isPrimary=true,并且被指派了一个pointerId。
- 第二个和后续的更多个手指触摸(假设第一个手指仍在触摸):
pointerdown事件触发,isPrimary=false,并且每一个触摸都被指派了不同的pointerId。
最终,如果有五个手指放在了屏幕上,我们会得到 5 个pointerdown 事件,和 5 个pointerId。
3.4.1 指针中断 - pointercancel
pointercancel 事件在触发后,会取消当前处在活跃状态的指针。该事件常常用在主动中断指针,使被中断的指针不会继续触发其他指针事件:
导致指针中断的可能原因如下:
- 指针设备硬件在物理层面上被禁用。
- 设备方向旋转(例如给平板转了个方向)。
- 浏览器开始处理这一交互。比如将其看作是一个专门的鼠标手势或缩放操作等。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
pointercancel事件。 - 我们可以通过阻止浏览器默认行为,来防止
pointercancel事件的触发。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
如何阻止阻止浏览器默认行为,来防止 pointercancel 事件的触发:
- 阻止原生的拖放操作发生:
- JS 中设置:
someElement.ondragstart = () => false,也适用于鼠标事件。
- JS 中设置:
- 阻止其他触摸相关的浏览器默认操作:
- CSS 中设置:
#someElement { touch-action: none }来阻止它们。
- CSS 中设置:
1.1.2 指针捕获 - setPointerCapture()
指针捕获允许一个特定的指针事件(PointerEvent) 事件从一个事件触发时候的目标重定位到另一个目标上。这个功能可以确保一个元素可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素(比如,在滚动的时候)。
比如,在设置拖动一个小方块(box)的时候,指针事件在 document 上监听,一旦监听到指针处在 box 上时,可以使用指针捕获 (setPointerCapture) 把 event.target 重定向(指向)到 box 上,这样的好处有:
- 其他元素将不能再作为该 pointer 事件的目标了,其他元素的
pointerover,pointeroutpointerenter, 和pointerleave事件将不会被触发。接下来所有的指针事件,都会被重定向到 box 上。 - 确保 box 可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素。 比如在拖动划动条,鼠标经常会离开划动块儿的区域。利用指针捕获可以确保指向 box 的 pointer 事件一直在活跃状态。
- 即使用户在整个文档上移动指针,事件处理程序也将仅在
thumb上被调用。 此外,事件对象的坐标属性,例如clientX/clientY仍将是正确的,捕获仅影响target/currentTarget。
语法:
elem.setPointerCapture(pointerId) :指针捕获。
- 将给定的
pointerId绑定到elem。 在调用之后,所有具有相同pointerId的指针事件,都将elem作为目标(就像事件发生在elem上一样),无论elem在文档中的实际位置是什么。
elem.releasePointerCapture(pointerId):取消指针捕获。
绑定会在以下情况下被移除:
- 当
pointerup或pointercancel事件出现时; - 当
elem被从文档中移除后; - 当
elem.releasePointerCapture(pointerId)被调用后。
3.5 键盘事件
keydown 事件:当一个按键被按下时触发;
keyup 事件:当一个按键被释放时触发。
3.5.1 事件对象
event.key 属性:获取当前按键的字符,会受大小写 (shift) 的影响而保存不同字母。
event.code 属性:获取当前按键的“物理按键代码”。和按键一一对应,不会改变。
- 区分,
event.code准确地标明了哪个键被按下。如两个 Shift 键,会区分"ShiftRight","ShiftLeft"。event.key只标明按键的“含义”,即它是什么(一个“Shift”),随着OS不同会因此改变:cmd。
比如,按键 “Z” 的效果:
| Key | event.key | event.code |
|---|---|---|
| Z | z(小写) | KeyZ |
| Shift+Z | Z(大写) | KeyZ |
更多举例:
| Key | event.key | event.code |
|---|---|---|
| F1 | F1 | F1 |
| Backspace | Backspace | Backspace |
| Shift | Shift | ShiftRight 或 ShiftLeft |
event.code 按键代码:
-
字符键:
"Key<letter>":"KeyA","KeyB"等。 -
数字键:
"Digit<number>":"Digit0","Digit1"等。- 特殊按键,为按键的名字:
"Enter","Backspace","Tab","ShiftLeft"等。
- 特殊按键,为按键的名字:
-
更多:UI 事件代码规范 。
3.5.2 兼容性问题
event.key 会受到不同OS平台的影响,而呈现不同的效果。例如在使用“撤销”组合按下时:
- MacOS:是
Cmd + Z。 - Windows:是
Ctrl + Z。
event.code 会受到不同键盘布局的影响,相同的按键位置却收到不同的结果,同样在“撤销”组合按下时:
- 美式布局 (QWERTY):是正常的,按下 Z 时,
event.code等于KeyZ。 - 德式布局 (QWERTZ):按下 Y 时,
event.code也等于KeyZ。

因此,event.code 可能由于特殊键盘布局,会错误的匹配字符。幸运的是,这种情况只发生在几个代码上,例如 keyA,keyQ,keyZ,可以在 规范 中找到该列表。
总结:
-
如果频繁切换语言(德式键盘、美式键盘),使用
event.key更好; -
如果想兼容更多操作系统(MacOS、Win),使用
event.code更好。
3.5.3 自动重复
触发自动重复, event.repeat 属性会被设置为 true。
如果按下一个键足够长的时间,它就会开始“自动重复”:
-
keydown会被一次又一次地触发; -
当按键被释放时,最终会得到
keyup。因此,有很多keydown却只有一个keyup是很正常的。 -
同时,对于由自动重复触发的事件,
event对象的event.repeat属性被设置为true。